package ferp.center.server.ranking;

import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.taskqueue.TaskOptions;
import ferp.center.network.ResponseLeaderboardGet;
import ferp.center.server.Action;
import ferp.center.server.entity.Range;
import ferp.center.server.entity.RangeTree;
import ferp.center.server.entity.Rating;

import javax.persistence.EntityManager;
import java.util.LinkedList;
import java.util.List;

/**
 * User: igorgok
 * Date: 5/28/13 9:50 AM
 */
public class Ranker
{
  public static Queue queue() {return QueueFactory.getQueue(QUEUE);}
  public static TaskHandle queue(TaskOptions to) {return queue().add(to.header("Host", Action.BASE));}

  // update tree with the given rating
  public static void update(EntityManager em, Rating rating)
  {
    // the rating is already in the tree
    Range leaf = rating.getRange(em);

    if (leaf != null)
    {
      // only if we the score has changed, otherwise do nothing
      if (rating.getValue() == leaf.getMin())
        return;

      // remove the leaf unless there are other ratings attached to it
      remove(em, leaf);
    }

    // get appropriate range tree
    String configuration = rating.getConfiguration();
    RangeTree tree = Lookup.tree(em, configuration);

    if (tree == null)
    {
      // no tree exists for this configuration - create it
      tree = Create.tree(em, configuration);

      Action.logger().info("created new tree for configuration [" + configuration + "]");
    }

    // add the rating to the tree
    add(em, tree, rating);
  }

  public static long rank(EntityManager em, Rating rating)
  {
    LinkedList<Range> ancestors = ancestors(em, rating);

    if (ancestors.size() > 0)
    {
      long key = ancestors.removeFirst().getKey().getId();
      int rank = 0;

      for (Range r : ancestors)
      {
        Range.Children children = r.children();

        for (Range.Child c : children.sorted())
          if (c.key == key)
          {
            key = r.getKey().getId();
            break;
          }
          else
            rank += c.size;
      }

      return rank;
    }

    return ResponseLeaderboardGet.NO_RANK;
  }

  public static List<List<Rating>> top(EntityManager em, String configuration, int size) {return top(em, configuration, size, null);}
  public static List<List<Rating>> top(EntityManager em, String configuration, int size, LinkedList<Range> el)
  {
    RangeTree tree = Lookup.tree(em, configuration);

    if (tree != null)
    {
      // find the top rating
      Range r = tree.getRoot(em);

      if (r != null)
      {
        LinkedList<List<Rating>> table = new LinkedList<List<Rating>>();

        top(em, r, table, el, size);

        return table;
      }
    }

    return null;
  }

  // remove leaf node from the tree
  public static void remove(EntityManager em, Range leaf)
  {
    // update number of ratings in the leaf and check if we need to remove it completely
    leaf.setSize(leaf.getSize() - 1);

    // remove the leaf
    if (leaf.getSize() <= 0)
    {
      LinkedList<Range> ancestors = ancestors(em, leaf);

      Action.logger().info("removing leaf " + leaf.getKey() + ", max: " + leaf.getMax() + ", ancestors: " + ancestors.size());

      // update ancestor chain
      if (ancestors.size() > 0)
      {
        Range target = ancestors.removeFirst();
        Range.Children children = target.children();

        // update target children (and size)
        children.remove(leaf.getKey().getId());
        target.update(children);

        update(target, ancestors);
      }

      // remove the leaf range object
      em.remove(leaf);
    }
    else
      Action.logger().info("leaf " + leaf.getKey() + " not removed, remaining ratings: " + leaf.getSize());
  }

  // -------------------------------------------

  private static final String QUEUE = "ranker";
  private static int ARITY = 10;

  // add rating which is not participating in the tree
  private static void add(EntityManager em, RangeTree tree, Rating rating)
  {
    LinkedList<Range> ancestors = new LinkedList<Range>();
    // find matching leaf range (ancestor list starts from the target range)
    Range leaf = Lookup.leaf(em, tree, rating, ancestors);

    if (leaf == null)
    {
      Range target = ancestors.removeFirst();
      Range.Children children = target.children();

      // create leaf
      leaf = Create.leaf(em, rating);

      // connect new leaf to the appropriate range
      // add the leaf range to the target range and update its size
      // for target the leaf's size
      children.add(leaf.getKey().getId(), 1, leaf.getMax());
      target.update(children);
      leaf.setParent(target.getKey());

      // update target and its ancestor sizes
      update(target, ancestors);

      // check if the range split required
      if (children.size() > ARITY)
        // split must update children in the target
        split(em, tree, target, children, ancestors);
    }

    // connect rating to the leaf (no need to update leaf.children field)
    rating.setRange(leaf);
    // count ratings in the leaf, the size is not propagated upwards
    leaf.setSize(leaf.getSize() + 1);
  }

  private static void update(Range range, LinkedList<Range> ancestors)
  {
    // we always add only one rating at the time
    for (Range ancestor : ancestors)
    {
      Range.Children children = ancestor.children();

      // update specific child size
      children.update(range);
      ancestor.update(children);

      // next ancestor
      range = ancestor;
    }
  }

  private static void split(EntityManager em, RangeTree tree, Range target, Range.Children children, LinkedList<Range> ancestors)
  {
    Range sibling = Range.sibling(target);
    // split the list (in memory)
    Range.Children c2 = children.split();
    // first key in c2
    //long fk = c2.first().key;

    // set ranges
    // todo: replace em.find(fk).getMax() with with c2.first().max
    //sibling.setMax(em.find(Range.class, fk).getMax());
    sibling.setMax(c2.first().max);
    sibling.setMin(target.getMin());
    target.setMin(sibling.getMax());

    // update children
    target.update(children);
    sibling.update(c2);

    // add the new range
    em.persist(sibling);
    em.refresh(sibling);

    // update parent in those who moved to the new range
    for (Range.Child child : c2.sorted())
      em.find(Range.class, child.key).setParent(sibling.getKey());

    // root node split?
    if (ancestors.size() > 0)
    {
      // update parent's children information
      Range parent = ancestors.removeFirst();

      children = parent.children();
      // update split child size
      children.update(target);
      // add the new range to its parent's child list
      children.add(sibling.getKey().getId(), sibling.getSize(), sibling.getMax());
      parent.update(children);

      if (children.size() > ARITY)
        split(em, tree, parent, children, ancestors);
    }
    else
      // root node split
      Create.root(em, tree, target, sibling);
  }

  public static LinkedList<Range> ancestors(EntityManager em, Rating rating)
  {
    Range parent = rating.getRange(em);
    LinkedList<Range> ancestors = ancestors(em, parent);

    ancestors.addFirst(parent);

    return ancestors;
  }

  public static LinkedList<Range> ancestors(EntityManager em, Range range)
  {
    LinkedList<Range> ancestors = new LinkedList<Range>();

    if (range != null)
      for (Range r = range.getParent(em); r != null; r = r.getParent(em))
        ancestors.add(r);

    return ancestors;
  }

  private static void top(EntityManager em, Range range, LinkedList<List<Rating>> table, LinkedList<Range> el, int size)
  {
    int i = 0;
    Range.Children children = range.children();

    switch (range.getLevel())
    {
      case 0:
        List<Rating> ratings = Lookup.ratings(em, range);

        //Action.logger().info("+++ place " + (table.size() + 1) + ": " + ratings.size() + " rating(s) +++");

        if (el != null && ratings.size() == 0)
          el.add(range);

        table.add(ratings);

        break;

      default:
        //Action.logger().info("@ " + range.getLevel() + " range [" + range.getMax() + ", " + range.getMin() + "] #" + children.size());

        for (Range.Child c : children.sorted())
        {
          //Action.logger().info("  child " + (++i) + " m:" + c.max + ", s: " + c.size);

          if (table.size() >= size)
            break;

          Range r = em.find(Range.class, c.key);

          if (r != null)
            top(em, r, table, el, size);
        }
    }
  }
}
